﻿using System;

using System.Net;
using System.Net.Sockets;

using System.Threading;

using System.Text;

namespace UdpAsyncEchoClient
{
    /*
     * !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
     * the client model used here is totally wrong, which will cause serious race condition
     * this model will launch multiple receiving threads to receive data simultaneously
     * which results in race condition to the internal receiving buffer of the socket
     * !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
        class UdpClient
        {
            private Socket m_udpsock;
            private byte[] m_recvBuffer = new byte[1024];
            private IPEndPoint m_svrEndpoint;

            static IPEndPoint CreateEndPoint(string hostNameOrAddress, int port)
            {
                if (string.IsNullOrEmpty(hostNameOrAddress))
                {
                    return new IPEndPoint(IPAddress.Any, port);
                }
                else
                {
                    IPAddress address;
                    if (IPAddress.TryParse(hostNameOrAddress, out address))
                        // Use the provided IP address.
                        return new IPEndPoint(address, port);
                    else
                        // Exception will occur if DNS lookup fails.
                        return new IPEndPoint(Dns.GetHostEntry(hostNameOrAddress).AddressList[0], port);
                }
            }

            public UdpClient(string svrAddress, int svrPort)
            {
                m_svrEndpoint = CreateEndPoint(svrAddress, svrPort);
                m_udpsock = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
            }

            public void Send(string inputMsg)
            {
                byte[] inputArray = Encoding.ASCII.GetBytes(inputMsg);
                m_udpsock.BeginSendTo(inputArray, 0, inputArray.Length, SocketFlags.None, m_svrEndpoint, OnSendCompleted, null);
                PrintCurrentThreadName("Send");
            }

            public void Close()
            {
                m_udpsock.Close();
            }

            /// <remark>
            /// [FROM MSDN]
            /// Before calling BeginReceiveFrom, you must explicitly bind the Socket to 
            /// a local endpoint using the Bind method, or BeginReceiveFrom will throw a SocketException.
            /// !!!!!!!!!!!!!!! but in this program, I don't call Bind before calling BeginReceiveFrom
            /// and a client UDP socket has no need to bind before sending and receiving
            /// and NO EXCEPTION IS THROWN !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
            /// </remark>
            private void OnSendCompleted(IAsyncResult asyncResult)
            {
                m_udpsock.EndSendTo(asyncResult);

                try
                {
                    EndPoint remoteEndPoint = CreateEndPoint(string.Empty, 0);
                    m_udpsock.BeginReceiveFrom(m_recvBuffer, 0, m_recvBuffer.Length, SocketFlags.None, ref remoteEndPoint, OnReceiveCompleted, null);
                }
                catch (SocketException ex)
                {
                    Console.WriteLine("!!!!! socket exception <{0},{1}> is caught, and continue", ex.SocketErrorCode, ex.ErrorCode);
                }

                PrintCurrentThreadName("OnSendCompleted");
            }

            private void OnReceiveCompleted(IAsyncResult asyncResult)
            {
                EndPoint remoteEndPoint = CreateEndPoint(string.Empty, 0);

                try
                {
                    int recvLength = m_udpsock.EndReceiveFrom(asyncResult, ref remoteEndPoint);

                    string echobackMsg = Encoding.ASCII.GetString(m_recvBuffer, 0, recvLength);
                    Console.WriteLine("[ASYNC CLIENT] ECHO BACK: {0}", echobackMsg);
                }
                catch (SocketException ex)
                {
                    Console.WriteLine("!!!!! socket exception <{0},{1}> is caught, and continue", ex.SocketErrorCode, ex.ErrorCode);
                }

                PrintCurrentThreadName("OnReceiveCompleted");
            }

            /// <summary>
            /// has no effect, for the thread name is null
            /// </summary>
            private void PrintCurrentThreadName(string prefix)
            {
                // Console.WriteLine("################ <{0}>'s thread = {1}", prefix,Thread.CurrentThread.Name);
            }
        }
    */

    sealed class UdpClient
    {
        private Socket m_udpsock;
        private IPEndPoint m_svrEndpoint;
        private byte[] m_recvBuffer = new byte[1024];
        private bool m_readStarted = false;

        static IPEndPoint CreateEndPoint(string hostNameOrAddress, int port)
        {
            if (string.IsNullOrEmpty(hostNameOrAddress))
            {
                return new IPEndPoint(IPAddress.Any, port);
            }
            else
            {
                IPAddress address;
                if (IPAddress.TryParse(hostNameOrAddress, out address))
                    // Use the provided IP address.
                    return new IPEndPoint(address, port);
                else
                    // Exception will occur if DNS lookup fails.
                    return new IPEndPoint(Dns.GetHostEntry(hostNameOrAddress).AddressList[0], port);
            }
        }

        public UdpClient(string svrAddress, int svrPort)
        {
            m_svrEndpoint = CreateEndPoint(svrAddress, svrPort);
            m_udpsock = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
        }

        public void Send(string inputMsg)
        {
            byte[] inputArray = Encoding.ASCII.GetBytes(inputMsg);
            m_udpsock.SendTo(inputArray, 0, inputArray.Length, SocketFlags.None, m_svrEndpoint);

            StartRead();
        }

        public void Close()
        {
            m_udpsock.Close();
        }

        public void StartRead()
        {
            if (!m_readStarted)
            {
                Read();
                m_readStarted = true;
            }
        }

        private void Read()
        {
            try
            {
                EndPoint remoteEndPoint = CreateEndPoint(string.Empty, 0);
                m_udpsock.BeginReceiveFrom(m_recvBuffer, 0, m_recvBuffer.Length, SocketFlags.None, ref remoteEndPoint, OnReceiveCompleted, null);
            }
            catch (SocketException ex)
            {
                Console.WriteLine("!!!!! socket exception <{0},{1}> is caught, and continue", ex.SocketErrorCode, ex.ErrorCode);
            }
        }

        private void OnReceiveCompleted(IAsyncResult asyncResult)
        {
            EndPoint remoteEndPoint = CreateEndPoint(string.Empty, 0);
            try
            {
                int recvLength = m_udpsock.EndReceiveFrom(asyncResult, ref remoteEndPoint);
                string echobackMsg = Encoding.ASCII.GetString(m_recvBuffer, 0, recvLength);
                Console.WriteLine("[ASYNC CLIENT] ECHO BACK: {0}", echobackMsg);
            }
            catch (SocketException ex)
            {
                Console.WriteLine("!!!!! socket exception <{0},{1}> is caught, and continue", ex.SocketErrorCode, ex.ErrorCode);
            }

            // post another asynchronous read
            Read();
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            UdpClient client = new UdpClient("localhost", 6001);
            /**
             * !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
             * "Bind" should be called before "BeginReceiveFrom", just as what MSDN tell us
             * but another alternative is that you can first call "SendTo", then you can successfully
             * call "BeginReceiveFrom"
             * that is to say that you must first call "Bind" or "SendTo"(I think "BeginSendTo" is suggested,
             * because you can not assure "BeginReceiveFrom" is executed precisely after "BeginSendTo")
             * before "BeginReceiveFrom"
             * !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
             */
            // client.Start();

            string strInput;
            while (true)
            {
                strInput = Console.ReadLine();
                if (strInput.Equals("exit", StringComparison.OrdinalIgnoreCase))
                    break;

                client.Send(strInput);
            }

            client.Close();
            Console.WriteLine("client is closed,program exits");
        }
    }
}
